Skip to content

feat: add OpenAI to Anthropic translator#2127

Open
ajac-zero wants to merge 26 commits into
envoyproxy:mainfrom
ajac-zero:add-openai-to-anthropic-translator
Open

feat: add OpenAI to Anthropic translator#2127
ajac-zero wants to merge 26 commits into
envoyproxy:mainfrom
ajac-zero:add-openai-to-anthropic-translator

Conversation

@ajac-zero

@ajac-zero ajac-zero commented May 11, 2026

Copy link
Copy Markdown
Contributor

Description

This commit adds OpenAI-compatible Chat Completions support for the native Anthropic provider. It translates /v1/chat/completions requests to Anthropic's /v1/messages API, including non-streaming responses, streaming responses, and errors.

The native Anthropic implementation is a small adapter around the established GCP Anthropic translator. It reuses shared request conversion, response translation, streaming, tracing, redaction, and Anthropic JSON error handling while handling the direct provider's transport differences:

  • placing the model in the request body
  • targeting /v1/messages
  • setting the anthropic-version header
  • labeling non-JSON fallback errors for the native provider
  • reporting the response model from native Anthropic streaming events

The change also adds focused translator and data-plane coverage and updates the supported-provider documentation and Anthropic Chat Completions example.

Related Issues/PRs (if applicable)

Fixes #1936

Special notes for reviewers (if applicable)

GCP Anthropic is used as the implementation base because both providers return Anthropic Message JSON and native Anthropic SSE. AWS Anthropic adds Bedrock-specific paths, error headers, and binary EventStream framing, so it is not a suitable base for the direct provider adapter.

A provider-neutral Anthropic core with separate native, GCP, and AWS transport adapters may be a useful future refactor, but it is intentionally outside this PR to avoid modifying established provider paths.

This change was developed with assistance from gpt-5.5 and gpt-5.6, with all code reviewed and tested by me.

ajac-zero added 2 commits May 5, 2026 19:02
This adds support for translating OpenAI ChatCompletion requests to
Anthropic's direct Messages API, completing the bidirectional translation
support between OpenAI and Anthropic.

Previously, the gateway supported:
- Anthropic → OpenAI (anthropic_openai.go)
- OpenAI → AWS Anthropic (openai_awsanthropic.go)
- OpenAI → GCP Anthropic (openai_gcpanthropic.go)

This commit adds:
- OpenAI → Anthropic direct API (openai_anthropic.go)

Changes:
- Add new translator in internal/translator/openai_anthropic.go
  - Converts OpenAI requests to Anthropic Messages API format
  - Supports streaming and non-streaming requests
  - Handles tool use, images, and all message types
  - Includes response redaction for debug logging
  - Uses default Anthropic API version 2023-06-01

- Add comprehensive test suite in internal/translator/openai_anthropic_test.go
  - 14 test cases covering all functionality
  - Tests request/response translation, streaming, errors, redaction
  - All tests passing

- Register translator in internal/endpointspec/endpointspec.go
  - Add APISchemaAnthropic case to ChatCompletionsEndpointSpec.GetTranslator

- Update internal/endpointspec/endpointspec_test.go
  - Add APISchemaAnthropic to supported schemas test list

The new translator follows the same patterns as existing cloud-based
Anthropic translators but is simpler since it targets the direct API:
- Uses standard /v1/messages endpoint
- Includes model and stream fields in request body
- No cloud-specific wrapper handling needed

Signed-off-by: Anibal Angulo <ajcardoza2000@gmail.com>
Signed-off-by: Anibal Angulo <ajcardoza2000@gmail.com>
@ajac-zero
ajac-zero force-pushed the add-openai-to-anthropic-translator branch from 21ae3d7 to 501dbaf Compare May 11, 2026 19:26
@ajac-zero ajac-zero changed the title feat: Add OpenAI to Anthropic translator feat: add OpenAI to Anthropic translator May 11, 2026
@PatilHrushikesh

Copy link
Copy Markdown
Contributor

are you still working on this?

@ajac-zero

Copy link
Copy Markdown
Contributor Author

I confirm I am still working on this!

@codecov-commenter

codecov-commenter commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.88%. Comparing base (ea1c886) to head (5bc63ab).

Files with missing lines Patch % Lines
internal/translator/openai_anthropic.go 83.78% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2127      +/-   ##
==========================================
+ Coverage   84.85%   84.88%   +0.02%     
==========================================
  Files         151      152       +1     
  Lines       22086    22131      +45     
==========================================
+ Hits        18742    18785      +43     
- Misses       2213     2214       +1     
- Partials     1131     1132       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ajac-zero
ajac-zero force-pushed the add-openai-to-anthropic-translator branch from 62d94a4 to d096487 Compare June 11, 2026 22:27
Signed-off-by: Anibal Angulo <ajcardoza2000@gmail.com>
…or' into add-openai-to-anthropic-translator

Signed-off-by: Anibal Angulo <ajcardoza2000@gmail.com>
Signed-off-by: Anibal Angulo <ajcardoza2000@gmail.com>
@ajac-zero
ajac-zero force-pushed the add-openai-to-anthropic-translator branch 3 times, most recently from 73ca92b to 0115e3f Compare June 17, 2026 17:27
…ve redaction conflicts

Merge main (qxqlsrvm) into add-openai-to-anthropic-translator (tqsrmsum).

Conflicts were in RedactBody for the AWS Bedrock and GCP Anthropic translators,
where a provider-specific inline redaction implementation conflicted with the
cleanup that replaced it with the shared redactAnthropicChatCompletionResponse
helper.

Resolution: use the shared helper in both translators. Also updated
anthropic_helper.go to preserve tool function names (keeping only argument
redaction), consistent with the approach already used in openai_openai.go, and
updated the corresponding test expectation.

Signed-off-by: ajac-zero <ajcardoza2000@gmail.com>
OpenAI's chat completions API rejects requests that include a "thinking"
field with "thinking is not allowed". The thinking config is an
Anthropic-specific concept; OpenAI reasoning models use reasoning_effort
instead, which is a separate, backend-specific setting.

Remove anthropicThinkingToOpenAI and its usage in
buildOpenAIChatCompletionRequest. Thinking blocks in assistant message
history (for multi-turn conversations) are intentionally preserved, as
they carry context, not backend configuration.

Update TestBuildOpenAIChatCompletionRequest_ThinkingConfig and
TestAnthropicToOpenAITranslator_RequestBody_ThinkingConfig to assert
that thinking config is dropped for all variants (enabled/disabled/adaptive).

Signed-off-by: ajac-zero <ajcardoza2000@gmail.com>
Signed-off-by: ajac-zero <ajcardoza2000@gmail.com>
Signed-off-by: ajac-zero <ajcardoza2000@gmail.com>
@ajac-zero
ajac-zero force-pushed the add-openai-to-anthropic-translator branch from 12c86ec to 3743023 Compare June 17, 2026 19:16
Signed-off-by: ajac-zero <ajcardoza2000@gmail.com>
Signed-off-by: ajac-zero <ajcardoza2000@gmail.com>
@ajac-zero
ajac-zero marked this pull request as ready for review June 18, 2026 00:37
@ajac-zero
ajac-zero requested a review from a team as a code owner June 18, 2026 00:37
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jun 18, 2026
@ajac-zero
ajac-zero force-pushed the add-openai-to-anthropic-translator branch from 90c56e6 to 443f4c6 Compare June 18, 2026 06:39
@ajac-zero

Copy link
Copy Markdown
Contributor Author

@PatilHrushikesh Ready for review when you get the chance!

@ajac-zero

Copy link
Copy Markdown
Contributor Author

I've been dogfooding this implementation for a bit in our deployment, and I found some rough edges that I think are worth mentioning here:

  1. Omitted max_tokens defaulted to 0 in OpenAI clients -> Received empty responses:
    This was a really confusing silent behavior. Since OpenAI clients set max_tokens to null by default, the gateway was defaulting that field to 0 before forwaring to Anthropic backends. A request with max_tokens=0 is actually an official technique to populate the prompt cache, but it results in 200 responses with content=None.
    The proposed solution is to add a validation in the openai->anthropic path that rejects requests that omit max_tokens, essentially turning it into a required field within this path. This was prefered in order to keep behavior explicit, instead of defaulting to an arbitrary number like 1024 which could silently affect responses.

  2. Missing mapping between thinking (Anthropic) and reasoning_effort (OpenAI):
    Unfortunately the mapping is not very straightforward, because thinking requires setting a specific int budget instead of low|medium|high like other APIs. For now, it can still be done from openai->anthropic via the extra_body field in requests:

from openai import OpenAI

client = OpenAI(
    base_url="http://<gateway-host>/v1",
    api_key="<gateway-api-key>",
)

response = client.chat.completions.create(
    model="claude-sonnet-4-6",
    messages=[
        {
            "role": "user",
            "content": "Explain why the sky is blue in one paragraph.",
        }
    ],
    max_tokens=1024,
    extra_body={
        "thinking": {
            "type": "enabled",
            "budget_tokens": 4096,
        }
    },
)

@JAORMX

JAORMX commented Jul 3, 2026

Copy link
Copy Markdown

Panel review — OpenAI → first-party Anthropic translator

I ran a three-axis panel review (Spec / Standards / Domain) against this PR, with a parallel comparison PR (#2284) as a reference. Overall this is a well-executed translator that slots cleanly into the existing family — the redaction consolidation and streaming-model tracking are the right calls. Below are the findings, ordered by priority.

A kind note up front: please add Closes #1936 to the PR body so the issue it fixes gets referenced and auto-closed — right now #1936 is untagged and hard to discover from the issue side.


Spec — does this implement what was asked?

Issue #1936 asks to stop the unsupported API schema: backend={Anthropic …} crash and add an OpenAI→Anthropic translator. Both are shipped.

Scope creep (all real, flagging for conscious sign-off):

  • max_tokens made required across ALL Anthropic paths — the check landed in the shared buildAnthropicParams, so it changes behavior for existing AWS + GCP Anthropic users, not just the new direct path. The issue only asked for the direct translator.
  • Anthropic thinking config forwarding removed in the reverse direction (openai_helper.go) — unrelated to Support for OpenAI to Anthropic schema translator #1936.
  • Redaction refactored across 3 sibling translators — reasonable, not asked for.
  • AWS error handling rewritten (openai_awsanthropic.go:110-142) — not implied by the spec.

Ship-blockers (1)

[High] The max_tokens 422 fires before body mutation, breaking the documented workaround

anthropic_helper.go rejects an omitted max_tokens from translator.RequestBody. That runs at processor_impl.go:338 and short-circuits to a 422 ImmediateResponse (processor_impl.go:340-346) before applyBodyMutation at processor_impl.go:378. So a route-level request-body mutation can no longer inject max_tokens to rescue such a request — yet site/docs/getting-started/connect-providers/aws-bedrock.md:278 explicitly tells operators:

"if your clients do not include it, you must inject it via a request body mutation."

An operator following the docs now gets the 422 regardless and has no thread to pull. Fix: either move the check to operate on the post-mutation body, or update the docs to say the client must send max_tokens and a body mutation cannot supply it.


Cross-confirmed (1)

[High] Behavior change with no disclosure: missing max_tokens now returns gateway 422 instead of backend 400, for GCP/AWS Anthropic backends

Previously a missing max_tokens against gcp-anthropicai / aws-anthropic returned HTTP 400 from the backend (the old data-plane tests documented this). After this PR the same request returns HTTP 422 from the gateway. This affects existing, already-supported backends (GCP/AWS Anthropic predate this PR). Operators with alerting keyed on 400, or clients retrying on backend 400, see a different signal with no release note. Worth a release-notes entry stating the status change and the new required field.


Mechanical fixes (3)

  • [Medium] max_tokens validation is in the wrong layer. buildAnthropicParams is a translation helper; reject-vs-forward is a translator-level policy. Embedding it in the shared path makes a loud cross-family behavior change silent — a fourth Anthropic-family translator added later will inherit the reject policy with no signal. Move the 3-line check into each translator's RequestBody, OR keep it in the helper with a one-line comment stating it's a deliberate cross-family policy + a release note.
  • [Medium] redactAnthropicChatCompletionResponse is mis-named and mis-located. It operates purely on *openai.ChatCompletionResponse — nothing Anthropic-specific. "Anthropic" in the name is wrong; it belongs with the OpenAI redaction surface (openai_openai.go, where redactReasoningContent lives), not anthropic_helper.go. Rename to redactChatCompletionResponse and relocate. The Rule-of-Three was passed long ago (5+ sites); this PR stopped at 3.
  • [Medium] openai_awsanthropic.go is the missing fourth Anthropic translator — it has no RedactBody / SetRedactionConfig and no debug-log block, unlike its three siblings the PR consolidated. Either wire it to the shared helper or leave a comment explaining the exclusion.

Judgement calls (3)

  • [Medium] Unbounded read of untrusted backend bodies (io.ReadAll with no io.LimitReader, openai_anthropic.go:76-87/:104-113, plus the AWS streaming path). CWE-400 / OWASP API4:2023. Requires a malicious upstream (semi-trusted per threat model), partially mitigated by Envoy's buffer limits + gRPC maxRecvMsgSize. This PR adds a new instance of a codebase-wide pattern. Fix is a one-line io.LimitReader wrapper at the boundary.
  • [Low] params.Model = o.requestModel reach-through — the helper has an undocumented "leaves Model unset" contract. The PR adds a good comment at the reach-through site; add the matching contract to buildAnthropicParams's doc comment. One line.
  • [Low] Constructor double-default on prefix. openai_anthropic.go defaults "" → "v1", but the only caller already passes schema.AnthropicPrefix() which itself defaults to "v1". Redundant, and diverges from the OpenAI sibling where "" yields no prefix. Drop the constructor-level default.

Polish (4)

  • [Low] Docs contradiction worsens. supported-providers.md says Anthropic = "Support only Native Anthropic messages endpoint" — now stale. supported-endpoints.md:402 already claims Anthropic ✅ Chat Completions. The Chat Completions provider list at supported-endpoints.md:37-42 omits Anthropic. Update both to match the new reality.
  • [Low] No runnable Anthropic Chat Completions example with max_tokens set — the existing example uses gpt-4o-mini with no max_tokens, which now 422s against Anthropic.
  • [Low] anthropic-version is hardcoded to 2023-06-01 with no override knob, unlike the GCP sibling which threads apiVersion. The Anthropic docs confirm the header is required and the SDK pins this version, so it's defensible — but PR translator: add OpenAI to first-party Anthropic translator #2284 shows the override is a one-liner.
  • [Info] Backend error messages pass through unredacted to the OpenAI client. Intentional and tested across the family; noted so it isn't re-flagged.

Test-coverage gaps (compared to the parallel #2284, which is being closed in favor of this one)

A few things #2284 did better that are worth lifting over:

  • Adversarial SSE test coverage. translator: add OpenAI to first-party Anthropic translator #2284's test file has dedicated cases for: empty content, tool_use without text, truncated/malformed SSE data lines, missing \n\n event boundaries with partial-buffer flush across calls, and mid-stream error event surfacing. This PR has none of these — its streaming tests are happy-path only. The streaming parser is shared infrastructure; a malformed-SSE regression here breaks all four Anthropic translators. This is the single biggest test-coverage gap.
  • Data-plane integration tests for the new path. translator: add OpenAI to first-party Anthropic translator #2284 adds 3 end-to-end cases (non-stream, stream, error) exercising /v1/chat/completions → Anthropic through the real extproc harness. This PR only retargets two existing "missing max_tokens" cases; it never proves the new translator works end-to-end through the harness.
  • Docs update. translator: add OpenAI to first-party Anthropic translator #2284 adds Anthropic to the supported-endpoints.md provider list; this PR leaves the contradiction above.

Summary

  • Spec: 0 missing, 4 scope-creep items (the max_tokens one is the load-bearing concern), 0 wrong.
  • Standards: 0 hard violations, 1 convention gap (Closes #1936 missing from PR body).
  • Domain: 1 ship-blocker (docs/workaround breakage), 1 cross-confirmed (undisclosed 400→422 behavior change), 3 mechanical, 3 judgement, 4 polish.

Most important single issue: the max_tokens 422 fires before body mutation, breaking the documented operator workaround at aws-bedrock.md:278. Fix the ordering or fix the docs before merge.

Great work overall — the design wins (configurable prefix, shared redaction, streaming response-model, thinking fix) are real and worth keeping. Happy to help land any of the mechanical fixes.

Anibal Angulo added 7 commits July 13, 2026 15:00
…ic-translator

Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
… main

The merge from main added a useResponseModel parameter to
newAnthropicStreamParser. Update the remaining test call sites that
were not part of the conflict resolution.

Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
Implement direct Anthropic as a small adapter around the existing GCP Anthropic translator. Reuse its response, streaming, tracing, and redaction behavior while overriding direct request construction and raw error labeling.\n\nRemove duplicated tests and unrelated changes to established AWS, GCP, and reverse translation paths.

Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
envoyproxy#1936

Add data-plane coverage for non-streaming, streaming, and error responses through the direct Anthropic backend. Document Chat Completions support and correct the runnable Anthropic example.\n\nClarify the shared request builder's model contract and keep API prefix defaulting at the schema boundary.

Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jul 13, 2026
@ajac-zero
ajac-zero marked this pull request as draft July 13, 2026 15:26
@ajac-zero

Copy link
Copy Markdown
Contributor Author

I substantially reworked this PR after recognizing that the existing OpenAI → GCP Anthropic translator already implements almost all the protocol behavior needed for first-party Anthropic.

The previous implementation introduced a separate, largely duplicated translator and accumulated several adjacent changes while the branch was open. This made the PR larger than necessary and increased review burden and regression risk.

Direct Anthropic is now a small adapter around the established GCP Anthropic translator:

  • Request parameter conversion remains shared through buildAnthropicParams.
  • Response translation, streaming, tracing, redaction, and Anthropic JSON error handling reuse the existing GCP path.
  • The direct adapter only handles the actual provider differences:
    • model placement in the request body
    • /v1/messages path construction
    • anthropic-version header
    • direct-provider fallback error type
    • response-model tracking from native Anthropic streaming events

GCP is the practical base because its response body and streaming transport closely match the first-party API: both return Anthropic Message JSON and native Anthropic SSE. AWS Anthropic would be a poorer base because it adds Bedrock-specific paths, error headers, and binary EventStream framing.

Longer term, a provider-neutral Anthropic core with separate native, GCP, and AWS transport adapters would be the cleanest architecture. Extracting that abstraction here would increase scope and unnecessarily modify established paths. Reusing the proven GCP implementation is the smallest and safest way to add the requested capability.

This pivot also addresses the main concerns raised by @JAORMX:

  • Removed the unrelated reverse Anthropic → OpenAI thinking change.
  • Removed the shared redaction refactor and AWS error-handling rewrite.
  • Avoided adding new unbounded response-body reads by reusing the existing response implementation.
  • Preserved existing AWS and GCP Anthropic behavior.
  • Added focused direct Anthropic data-plane coverage for non-streaming, streaming, and error responses.
  • Updated provider documentation and the runnable Anthropic Chat Completions example.
  • Documented the shared request builder’s model-placement contract.
  • Removed redundant constructor-level prefix defaulting.

The effective PR diff is now limited to the direct-provider adapter, the small shared streaming change needed to report Anthropic’s response model, focused tests, endpoint registration, and documentation. Complex behavior remains delegated to established code already exercised by the GCP Anthropic test suite.

I believe this version is substantially easier to review and less likely to introduce regressions while still fully resolving #1936.

@ajac-zero
ajac-zero marked this pull request as ready for review July 13, 2026 15:43
Signed-off-by: Anibal Angulo <anibal.angulo.cardoza@banorte.com>
@missBerg missBerg added the area/translation Provider/endpoint coverage and schema translation (incl. fidelity bugs) label Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/translation Provider/endpoint coverage and schema translation (incl. fidelity bugs) size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for OpenAI to Anthropic schema translator

5 participants